<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  
  <meta http-equiv="X-UA-Compatible" content="IE=edge" >
  <title>WebView性能、体验分析与优化 | Grx Blog</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <meta name="description" content="WebView性能、体验分析与优化在App开发中，内嵌WebView始终占有着一席之地。它能以较低的成本实现Android、iOS和Web的复用，也可以冠冕堂皇的突破苹果对热更新的封锁。然而便利性的同时，WebView的性能体验却备受质疑，导致很多客户端中需要动态更新等页面时不得不采用其他方案。以发展的眼光来看，功能的动态加载以及三端的融合将会是大趋势。那么如何克服WebView固有的问题呢？我们">
<meta property="og:type" content="article">
<meta property="og:title" content="WebView性能、体验分析与优化">
<meta property="og:url" content="http://yoursite.com/2018/09/04/WebView性能、体验分析与优化/index.html">
<meta property="og:site_name" content="Grx Blog">
<meta property="og:description" content="WebView性能、体验分析与优化在App开发中，内嵌WebView始终占有着一席之地。它能以较低的成本实现Android、iOS和Web的复用，也可以冠冕堂皇的突破苹果对热更新的封锁。然而便利性的同时，WebView的性能体验却备受质疑，导致很多客户端中需要动态更新等页面时不得不采用其他方案。以发展的眼光来看，功能的动态加载以及三端的融合将会是大趋势。那么如何克服WebView固有的问题呢？我们">
<meta property="og:image" content="https://tech.meituan.com/img/webviewspeed/time.png">
<meta property="og:image" content="https://tech.meituan.com/img/webviewspeed/blocking.png">
<meta property="og:image" content="https://tech.meituan.com/img/webviewspeed/chunk.png">
<meta property="og:image" content="https://tech.meituan.com/img/webviewspeed/demo.png">
<meta property="og:image" content="https://tech.meituan.com/img/webviewspeed/dest.png">
<meta property="og:updated_time" content="2018-09-05T07:37:28.608Z">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="WebView性能、体验分析与优化">
<meta name="twitter:description" content="WebView性能、体验分析与优化在App开发中，内嵌WebView始终占有着一席之地。它能以较低的成本实现Android、iOS和Web的复用，也可以冠冕堂皇的突破苹果对热更新的封锁。然而便利性的同时，WebView的性能体验却备受质疑，导致很多客户端中需要动态更新等页面时不得不采用其他方案。以发展的眼光来看，功能的动态加载以及三端的融合将会是大趋势。那么如何克服WebView固有的问题呢？我们">
<meta name="twitter:image" content="https://tech.meituan.com/img/webviewspeed/time.png">
  
    <link rel="alternative" href="/atom.xml" title="Grx Blog" type="application/atom+xml">
  
  
    <link rel="icon" href="/favicon.png">
  
  <link rel="stylesheet" href="/css/style.css">
</head>

<body>
  <div id="container">
    <div class="left-col">
    <div class="overlay"></div>
<div class="intrude-less">
	<header id="header" class="inner">
		<a href="/" class="profilepic">
			
			<img lazy-src="/img/rufei.jpg" class="js-avatar">
			
		</a>

		<hgroup>
		  <h1 class="header-author"><a href="/">Grx</a></h1>
		</hgroup>

		
		<p class="header-subtitle">自我管理，知识管理，时间管理，阅读，语音写作，思维导图</p>
		

		
			<div class="switch-btn">
				<div class="icon">
					<div class="icon-ctn">
						<div class="icon-wrap icon-house" data-idx="0">
							<div class="birdhouse"></div>
							<div class="birdhouse_holes"></div>
						</div>
						<div class="icon-wrap icon-ribbon hide" data-idx="1">
							<div class="ribbon"></div>
						</div>
						
						<div class="icon-wrap icon-link hide" data-idx="2">
							<div class="loopback_l"></div>
							<div class="loopback_r"></div>
						</div>
						
						
						<div class="icon-wrap icon-me hide" data-idx="3">
							<div class="user"></div>
							<div class="shoulder"></div>
						</div>
						
					</div>
					
				</div>
				<div class="tips-box hide">
					<div class="tips-arrow"></div>
					<ul class="tips-inner">
						<li>菜单</li>
						<li>标签</li>
						
						<li>友情链接</li>
						
						
						<li>关于我</li>
						
					</ul>
				</div>
			</div>
		

		<div class="switch-area">
			<div class="switch-wrap">
				<section class="switch-part switch-part1">
					<nav class="header-menu">
						<ul>
						
							<li><a href="/">主页</a></li>
				        
							<li><a href="/tags/life">随笔</a></li>
				        
							<li><a href="/archives">所有文章</a></li>
				        
						</ul>
					</nav>
					<nav class="header-nav">
						<div class="social">
							
								<a class="github" target="_blank" href="https://github.com/ruixiaoguo" title="github">github</a>
					        
								<a class="weibo" target="_blank" href="https://weibo.com/u/3984935859?refer_flag=1005055010_" title="weibo">weibo</a>
					        
								<a class="rss" target="_blank" href="#" title="rss">rss</a>
					        
								<a class="zhihu" target="_blank" href="#" title="zhihu">zhihu</a>
					        
						</div>
					</nav>
				</section>
				
				
				<section class="switch-part switch-part2">
					<div class="widget tagcloud" id="js-tagcloud">
						<a href="/tags/OC/" style="font-size: 10px;">-OC</a> <a href="/tags/Android/" style="font-size: 10px;">Android</a> <a href="/tags/Apple-Watch/" style="font-size: 10px;">Apple Watch</a> <a href="/tags/Flutter/" style="font-size: 13.33px;">Flutter</a> <a href="/tags/Life/" style="font-size: 10px;">Life</a> <a href="/tags/Lottie/" style="font-size: 10px;">Lottie</a> <a href="/tags/MMKV/" style="font-size: 10px;">MMKV</a> <a href="/tags/MonkeyKing/" style="font-size: 10px;">MonkeyKing</a> <a href="/tags/OC/" style="font-size: 20px;">OC</a> <a href="/tags/RAC/" style="font-size: 11.67px;">RAC</a> <a href="/tags/Swift/" style="font-size: 15px;">Swift</a> <a href="/tags/WKWebView/" style="font-size: 10px;">WKWebView</a> <a href="/tags/WebView/" style="font-size: 10px;">WebView</a> <a href="/tags/Xcode/" style="font-size: 18.33px;">Xcode</a> <a href="/tags/appledoc/" style="font-size: 10px;">appledoc</a> <a href="/tags/hexo/" style="font-size: 11.67px;">hexo</a> <a href="/tags/ios/" style="font-size: 10px;">ios</a> <a href="/tags/life/" style="font-size: 16.67px;">life</a> <a href="/tags/xcode/" style="font-size: 10px;">xcode</a>
					</div>
				</section>
				
				
				
				<section class="switch-part switch-part3">
					<div id="js-friends">
					
			          <a target="_blank" class="main-nav-link switch-friends-link" href="http://blog.devtang.com">唐巧的博客</a>
			        
			          <a target="_blank" class="main-nav-link switch-friends-link" href="http://beyondvincent.com">破船之家</a>
			        
			          <a target="_blank" class="main-nav-link switch-friends-link" href="http://limboy.me">无网不剩</a>
			        
			          <a target="_blank" class="main-nav-link switch-friends-link" href="http://msching.github.io">码农人生</a>
			        
			          <a target="_blank" class="main-nav-link switch-friends-link" href="http://yulingtianxia.com">玉令天下</a>
			        
			          <a target="_blank" class="main-nav-link switch-friends-link" href="http://blog.leichunfeng.com">雷纯锋的技术博客</a>
			        
			          <a target="_blank" class="main-nav-link switch-friends-link" href="http://jax.cnblogs.com">包建强</a>
			        
			          <a target="_blank" class="main-nav-link switch-friends-link" href="http://blog.cnbang.net">bang</a>
			        
			        </div>
				</section>
				

				
				
				<section class="switch-part switch-part4">
				
					<div id="js-aboutme">知识管理，时间管理，自我管理，阅读，语音写作，思维导图，Wiz，TimeMeter</div>
				</section>
				
			</div>
		</div>
	</header>				
</div>

    </div>
    <div class="mid-col">
      <nav id="mobile-nav">
  	<div class="overlay">
  		<div class="slider-trigger"></div>
  		<h1 class="header-author js-mobile-header hide">Grx</h1>
  	</div>
	<div class="intrude-less">
		<header id="header" class="inner">
			<div class="profilepic">
			
				<img lazy-src="/img/rufei.jpg" class="js-avatar">
			
			</div>
			<hgroup>
			  <h1 class="header-author">Grx</h1>
			</hgroup>
			
			<p class="header-subtitle">自我管理，知识管理，时间管理，阅读，语音写作，思维导图</p>
			
			<nav class="header-menu">
				<ul>
				
					<li><a href="/">主页</a></li>
		        
					<li><a href="/tags/life">随笔</a></li>
		        
					<li><a href="/archives">所有文章</a></li>
		        
		        <div class="clearfix"></div>
				</ul>
			</nav>
			<nav class="header-nav">
				<div class="social">
					
						<a class="github" target="_blank" href="https://github.com/ruixiaoguo" title="github">github</a>
			        
						<a class="weibo" target="_blank" href="https://weibo.com/u/3984935859?refer_flag=1005055010_" title="weibo">weibo</a>
			        
						<a class="rss" target="_blank" href="#" title="rss">rss</a>
			        
						<a class="zhihu" target="_blank" href="#" title="zhihu">zhihu</a>
			        
				</div>
			</nav>
		</header>				
	</div>
</nav>

      <div class="body-wrap"><article id="post-WebView性能、体验分析与优化" class="article article-type-post" itemscope itemprop="blogPost">
  
    <div class="article-meta">
      <a href="/2018/09/04/WebView性能、体验分析与优化/" class="article-date">
  	<time datetime="2018-09-04T08:28:47.000Z" itemprop="datePublished">2018-09-04</time>
</a>
    </div>
  
  <div class="article-inner">
    
      <input type="hidden" class="isFancy" />
    
    
      <header class="article-header">
        
  
    <h1 class="article-title" itemprop="name">
      WebView性能、体验分析与优化
    </h1>
  

      </header>
      
      <div class="article-info article-info-post">
        
	<div class="article-tag tagcloud">
		<ul class="article-tag-list"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/WebView/">WebView</a></li></ul>
	</div>

        

        <div class="clearfix"></div>
      </div>
      
    
    <div class="article-entry" itemprop="articleBody">
      
        <h1 id="WebView性能、体验分析与优化"><a href="#WebView性能、体验分析与优化" class="headerlink" title="WebView性能、体验分析与优化"></a>WebView性能、体验分析与优化</h1><p>在App开发中，内嵌WebView始终占有着一席之地。它能以较低的成本实现Android、iOS和Web的复用，也可以冠冕堂皇的突破苹果对热更新的封锁。<br>然而便利性的同时，WebView的性能体验却备受质疑，导致很多客户端中需要动态更新等页面时不得不采用其他方案。<br>以发展的眼光来看，功能的动态加载以及三端的融合将会是大趋势。那么如何克服WebView固有的问题呢？我们将从性能、内存消耗、体验、安全几个维度，来系统的分析客户端默认WebView的问题，以及对应的优化方案。<br><a id="more"></a></p>
<h2 id="性能"><a href="#性能" class="headerlink" title="性能"></a>性能</h2><p>对于WebView的性能，给人最直观的莫过于：打开速度比native慢。</p>
<p>是的，当我们打开一个WebView页面，页面往往会慢吞吞的loading很久，若干秒后才出现你所需要看到的页面。</p>
<p>这是为什么呢？</p>
<p>对于一个普通用户来讲，打开一个WebView通常会经历以下几个阶段：</p>
<blockquote>
<p>交互无反馈<br>到达新的页面，页面白屏<br>页面基本框架出现，但是没有数据；页面处于loading状态<br>出现所需的数据<br>如果从程序上观察，WebView启动过程大概分为以下几个阶段：</p>
</blockquote>
<p><img src="https://tech.meituan.com/img/webviewspeed/time.png" alt="avatar"> </p>
<p>如何缩短这些过程的时间，就成了优化WebView性能的关键。</p>
<p>接下来我们逐一分析各个阶段的耗时情况，以及需要注意的优化点。</p>
<h2 id="WebView初始化"><a href="#WebView初始化" class="headerlink" title="WebView初始化"></a>WebView初始化</h2><p>当App首次打开时，默认是并不初始化浏览器内核的；只有当创建WebView实例的时候，才会创建WebView的基础框架。</p>
<p>所以与浏览器不同，App中打开WebView的第一步并不是建立连接，而是启动浏览器内核。</p>
<p>我们来分析一下这段耗时到底需要多久。</p>
<h2 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h2><p>针对WebView的初始化时间，我们可以定义两个指标：</p>
<ul>
<li>首次初始化时间：客户端冷启动后，第一次打开WebView，从开始创建WebView到开始建立网络连接之间的时间。</li>
<li>二次初始化时间：在打开过WebView后，退出WebView，再重新打开WebView，从开始创建WebView到开始建立网络连接之间的时间。<h2 id="测试数据："><a href="#测试数据：" class="headerlink" title="测试数据："></a>测试数据：</h2></li>
</ul>
<blockquote>
<p>测试系统1： iOS模拟器，Titans 10.0.7</p>
<p>测试系统2： OPPO R829T Android 4.2.2</p>
<p>测试方式：测试10次取平均值</p>
<p>测试App：美团外卖</p>
<p>单位：ms</p>
</blockquote>
<p>首次初始化时间    二次初始化时间<br>iOS（UIWebView）    306.56    76.43<br>iOS（WKWebView）    763.26    457.25<br>Android    192.79 *    142.53</p>
<blockquote>
<p> Android外卖客户端启动后会在后台开启WebView进程，故并不是完全新建WebView时间。</p>
</blockquote>
<h2 id="这意味着什么呢？"><a href="#这意味着什么呢？" class="headerlink" title="这意味着什么呢？"></a>这意味着什么呢？</h2><p>作为前端工程师，统计了无数次的页面打开时间，都是以网络连接开始作为起点的。</p>
<p>很遗憾的通知您：WebView中用户体验到的打开时间需要再增加70~700ms。</p>
<p>于是我们找到了“为什么WebView总是很慢”的原因之一：</p>
<ul>
<li>在浏览器中，我们输入地址时（甚至在之前），浏览器就可以开始加载页面。</li>
<li>而在客户端中，客户端需要先花费时间初始化WebView完成后，才开始加载。<br>而这段时间，由于WebView还不存在，所有后续的过程是完全阻塞的。可以这样形容WebView初始化过程：</li>
</ul>
<p><img src="https://tech.meituan.com/img/webviewspeed/blocking.png" alt="avatar"> </p>
<p>那么有哪些解决办法呢？</p>
<h2 id="怎么优化"><a href="#怎么优化" class="headerlink" title="怎么优化"></a>怎么优化</h2><p>由于这段过程发生在native的代码中，单纯靠前端代码是无法优化的；大部分的方案都是前端和客户端协作完成，以下是几个业界采用过的方案。</p>
<h2 id="全局WebView"><a href="#全局WebView" class="headerlink" title="全局WebView"></a>全局WebView</h2><p>方法：</p>
<ul>
<li>在客户端刚启动时，就初始化一个全局的WebView待用，并隐藏；</li>
<li>当用户访问了WebView时，直接使用这个WebView加载对应网页，并展示。</li>
</ul>
<p>这种方法可以比较有效的减少WebView在App中的首次打开时间。当用户访问页面时，不需要初始化WebView的时间。</p>
<p>当然这也带来了一些问题，包括：</p>
<ul>
<li>额外的内存消耗。</li>
<li>页面间跳转需要清空上一个页面的痕迹，更容易内存泄露。<br>【参考东软专利 - 加载网页的方法及装置 CN106250434A】</li>
</ul>
<h2 id="客户端代理数据请求"><a href="#客户端代理数据请求" class="headerlink" title="客户端代理数据请求"></a>客户端代理数据请求</h2><p>方法：</p>
<ul>
<li>在客户端初始化WebView的同时，直接由native开始网络请求数据；</li>
<li>当页面初始化完成后，向native获取其代理请求的数据。</li>
</ul>
<p>此方法虽然不能减小WebView初始化时间，但数据请求和WebView初始化可以并行进行，总体的页面加载时间就缩短了；缩短总体的页面加载时间：</p>
<p>【参考腾讯分享：<a href="https://mp.weixin.qq.com/s/evzDnTsHrAr2b9jcevwBzA" target="_blank" rel="external">70%以上业务由H5开发，手机QQ Hybrid 的架构如何优化演进？</a>】</p>
<p>还有其他各种优化的方式，不再一一列举，总结起来都是围绕两点：</p>
<ul>
<li>在使用前预先初始化好WebView，从而减小耗时。</li>
<li>在初始化的同时，通过Native来完成一些网络请求等过程，使得WebView初始化不是完全的阻塞后续过程。</li>
</ul>
<h2 id="建立连接-服务器处理"><a href="#建立连接-服务器处理" class="headerlink" title="建立连接/服务器处理"></a>建立连接/服务器处理</h2><p>在页面请求的数据返回之前，主要有以下过程耗费时间。</p>
<ul>
<li>DNS</li>
<li>connection</li>
<li>服务器处理</li>
</ul>
<h2 id="分析-1"><a href="#分析-1" class="headerlink" title="分析"></a>分析</h2><p>以下为美团中活动页面的链接时间统计：</p>
<blockquote>
<p>统计： 美团的活动页面<br>内容值： n%分位值（ms）</p>
</blockquote>
<pre><code>DNS    connection    获取首字节
50%    1.3    71    172
90%    60    360    541
</code></pre><h2 id="优化"><a href="#优化" class="headerlink" title="优化"></a>优化</h2><p>这些时间都是发生在网页加载之前，但这并不意味着无法优化，有以下几种方法。</p>
<h2 id="DNS采用和客户端API相同的域名"><a href="#DNS采用和客户端API相同的域名" class="headerlink" title="DNS采用和客户端API相同的域名"></a>DNS采用和客户端API相同的域名</h2><p>DNS会在系统级别进行缓存，对于WebView的地址，如果使用的域名与native的API相同，则可以直接使用缓存的DNS而不用再发起请求图片。</p>
<p>以美团为例，美团的客户端请求域名主要位于api.meituan.com，然而内嵌的WebView主要位于 i.meituan.com。</p>
<p>当我们初次打开App时：</p>
<p>客户端首次打开都会请求api.meituan.com，其DNS将会被系统缓存。<br>然而当打开WebView的时候，由于请求了不同的域名，需要重新获取i.meituan.com的IP。<br>根据上面的统计，至少10%的用户打开WebView时耗费了60ms在DNS上面，如果WebView的域名与App的API域名统一，则可以让WebView的DNS时间全部达到1.3ms的量级。</p>
<p>静态资源同理，最好与客户端的资源域名保持一致。</p>
<h2 id="同步渲染采用chunk编码"><a href="#同步渲染采用chunk编码" class="headerlink" title="同步渲染采用chunk编码"></a>同步渲染采用chunk编码</h2><p>同步渲染时如果后端请求时间过长，可以考虑采用chunk编码，将数据放在最后，并优先将静态内容flush。对于传统的后端渲染页面，往往都是使用的【浏览器】–&gt; 【Web API】 –&gt; 【业务 API】的加载模式，其中后端时间就指的是Web API的处理时间了。在这里Web API一般有两个作用：</p>
<p>确定静态资源的版本。<br>根据用户的请求，去业务API获取数据。<br>而一般确定静态资源的版本往往是直接读取代码版本，基本无耗时；而主要的后端时间都花费在了业务API请求上面。</p>
<p>那么怎么优化利用这段时间呢？</p>
<p>在HTTP协议中，我们可以在header中设置 transfer-encoding:chunked 使得页面可以分块输出。如果合理设计页面，让head部分都是确定的静态资源版本相关内容，而body部分是业务数据相关内容，那么我们可以在用户请求的时候，首先将Web API可以确定的部分先输出给浏览器，然后等API完全获取后，再将API数据传输给浏览器。</p>
<p>下图可以直观的看出分chunk输出和一起输出的区别：</p>
<p><img src="https://tech.meituan.com/img/webviewspeed/chunk.png" alt="avatar"> </p>
<ul>
<li>如果采用普通方式输出页面，则页面会在服务器请求完所有API并处理完成后开始传输。浏览器要在后端所有API都加载完成后才能开始解析。</li>
<li>如果采用chunk-encoding: chunked，并优先将页面的静态部分输出；然后处理API请求，并最终返回页面，可以让后端的API请求和前端的资源加载同时进行。</li>
<li>两者的总共后端时间并没有区别，但是可以提升首字节速度，从而让前端加载资源和后端加载API不互相阻塞。</li>
</ul>
<h2 id="页面框架渲染"><a href="#页面框架渲染" class="headerlink" title="页面框架渲染"></a>页面框架渲染</h2><p>页面在解析到足够多的节点，且所有CSS都加载完成后进行首屏渲染。在此之前，页面保持白屏；在页面完全下载并解析完成之前，页面处于不完整展示状态。</p>
<h2 id="分析-2"><a href="#分析-2" class="headerlink" title="分析"></a>分析</h2><p>我们以一个美团的活动页面作为样例：</p>
<p>测试页面：<a href="http://i.meituan.com/firework/meituanxianshifengqiang" target="_blank" rel="external">http://i.meituan.com/firework/meituanxianshifengqiang</a></p>
<p>在Mac上面，模拟4G情况</p>
<p>页面样式：</p>
<p><img src="https://tech.meituan.com/img/webviewspeed/demo.png" alt="avatar"> </p>
<p>测试得到的时间耗费如下：</p>
<p>表1</p>
<pre><code>阶段    时间    大小    备注
DOM下载    58ms    29.5 KB    4G网络
DOM解析    12.5ms    198 KB    根据估算，在手机上慢2~5倍不等
CSS请求+下载    58ms    11.7 KB    4G网络（包含链接时间，CDN）
CSS解析    2.89ms    54.1 KB    根据估算，在手机上慢2~5倍不等
渲染    23ms    1361节点    根据估算，在手机上慢2~5倍不等
绘制    4.1ms        根据估算，在手机上慢2~5倍不等
合成    0.23ms        GPU处理
</code></pre><p>同时，对HTML的加载时间进行分析，可以得到如下时间点。</p>
<p>表2</p>
<pre><code>指标    时间    计算方法
HTML加载完成时间    218    performance.timing.responseEnd - performance.timing.fetchStart
HTML解析完成时间    330    performance.timing.domInteractive - performance.timing.fetchStart
</code></pre><h2 id="这意味着什么呢？-1"><a href="#这意味着什么呢？-1" class="headerlink" title="这意味着什么呢？"></a>这意味着什么呢？</h2><p>对于表1<br>可以看到，随着在网络优良的情况下，Dom的解析所占耗时比例还是不算低的，对于低端机器更甚。Layout时间也是首屏前耗时的大头，据猜测这与页面使用了rem作为单位有关（待进一步分析）。</p>
<p>对于表2，我们可以发现一个问题<br>一般来说HTML在开始接收到返回数据的时候就开始解析HTML并构建DOM树。如果没有JS（JavaScript）阻塞的话一般会相继完成。然而，在这里时间相差了90ms……也就是说，解析被阻塞了。</p>
<p>进一步分析可以发现，页面的header部分有这样的代码：</p>
<pre><code>.....
&lt;link href=&quot;//ms0.meituan.net/css/eve.9d9eee71.css&quot; rel=&quot;stylesheet&quot; onload=&quot;MT.pageData.eveTime=Date.now()&quot;/&gt;
&lt;script&gt;
window.fk = function (callback) {
require([&apos;util/native/risk.js&apos;], function (risk) {
risk.getFk(callback);
});
}
&lt;/script&gt;
&lt;/head&gt;
....
</code></pre><p>通常情况下，上面代码的link部分和script部分如果单独出现，都不会阻塞页面的解析：</p>
<ul>
<li>CSS不会阻止页面继续向下继续。</li>
<li><p>内联的JS很快执行完成，然后继续解析文档。<br>然而，当这两部分同时出现的时候，问题就来了。</p>
</li>
<li><p>CSS加载阻塞了下面的一段内联JS的执行，而被阻塞的内联JS则阻塞了HTML的解析。</p>
</li>
</ul>
<p>通常情况下，CSS不会阻塞HTML的解析，但如果CSS后面有JS，则会阻塞JS的执行直到CSS加载完成（即便JS是内联的脚本），从而间接阻塞HTML的解析。</p>
<h2 id="优化-1"><a href="#优化-1" class="headerlink" title="优化"></a>优化</h2><p>在页面框架加载这一部分，能够优化的点参照雅虎14条就够了；但注意不要犯错，一个小小的内联JS放错位置也会让性能下降很多。</p>
<ol>
<li>CSS的加载会在HTML解析到CSS的标签时开始，所以CSS的标签要尽量靠前。</li>
<li>但是，CSS链接下面不能有任何的JS标签（包括很简单的内联JS），否则会阻塞HTML的解析。</li>
<li>如果必须要在头部增加内联脚本，一定要放在CSS标签之前。<br>CSS带来的阻塞解析 </li>
</ol>
<h2 id="JS加载"><a href="#JS加载" class="headerlink" title="JS加载"></a>JS加载</h2><p>对于大型的网站来说，在此我们先提出几个问题：</p>
<ul>
<li>将全部JS代码打成一个包，造成首次执行代码过大怎么办？</li>
<li>将JS以细粒度打包，造成请求过多怎么办？</li>
<li>将JS按 “基础库” + “页面代码” 分别打包，要怎么界定什么是基础 代码，什么是页面代码；不同页面用的基础代码不一致怎么办？</li>
<li>单一文件的少量代码改的是否会导致缓存失效？</li>
<li>代码模块间有动态依赖，怎样合并请求。<br>关于这些问题的解决方案数量可能会比问题还多，而它们也各有优劣。</li>
</ul>
<p>具体分析太过复杂，鉴于篇幅原因在这里不做具体分析了。您可以期待我们的后续计划：BPM（浏览器包管理）。</p>
<h2 id="JS解析、编译、执行"><a href="#JS解析、编译、执行" class="headerlink" title="JS解析、编译、执行"></a>JS解析、编译、执行</h2><p>在PC互联网时代，人们似乎都快忘记了JS的解析和执行还需要消耗时间。确实，在几年前网速还在用kb衡量的时代里，JS的解析时间在整个页面的打开时间里只能算是九牛一毛。</p>
<p>然而，随着网速越来越快，而CPU的速度反而没有提升（从PC到手机），JS的时间开销就成为问题了。那么JS的编译和解析，在当今的页面上要消耗多少时间呢？</p>
<h2 id="分析-3"><a href="#分析-3" class="headerlink" title="分析"></a>分析</h2><p>我们用以下方式来检验JS代码的解析/编译和执行时间：</p>
<pre><code>&lt;script&gt;
window.t1 = performance.now()
&lt;/script&gt;
&lt;script&gt;
window.test = function () {
// test code
}
&lt;/script&gt;
&lt;script&gt;
window.t2 = performance.now()
test();
window.t3 = performance.now();

alert(&quot;编译耗时：&quot; + (t2 - t1));
alert(&quot;执行耗时：&quot; + (t3 - t2));
&lt;/script&gt;
</code></pre><p>将测试代码放入 【test code】 位置，然后在手机中执行；</p>
<ul>
<li>在t1~t2期间，JS代码仅仅声明了一个函数，主要时间会集中在解析和编译过程；</li>
<li>在t2~t3时间段内，执行test时时间主要为代码的执行时间<br>在首次启动客户端后，打开WebView的测试页面，我们可以得到如下的结果：</li>
</ul>
<blockquote>
<p>测试系统： iPhone6 iOS 10.2.1</p>
<p>测试系统： OPPO R829T Android 4.2.2</p>
<p>内容值： 编译时间（ms）/执行时间（ms）</p>
</blockquote>
<pre><code>系统    Zepto.js    Vue.js    React.js + ReactDOM.js
iOS    5.2 / 8    12.8 / 16.1    13.7 / 43.3
Android    13 / 40    43 / 127    26 / 353
</code></pre><p>当保持客户端进行不关闭情况下，关闭WebView并重新访问测试页面，再次测试得到如下结果：</p>
<pre><code>系统    Zepto.js    Vue.js    React.js + ReactDom.js
iOS    0.9 / 1.9    5 / 7.4    3.5 / 23
Android    5 / 9    17 / 12    25 / 60
</code></pre><p>执行时间指的是框架代码加载的页面的初始化时间，没有任何业务的调用。</p>
<h2 id="这意味着什么"><a href="#这意味着什么" class="headerlink" title="这意味着什么"></a>这意味着什么</h2><p>经过测试可以得出以下结论：</p>
<ul>
<li>偏重的框架，例如React，仅仅初始化的时间就会达到50ms ~ 350ms，这在对性能敏感的业务中时比较不利的。</li>
<li>在App的启动周期内，统一域名下的代码会被缓存编辑和初始化结果，重复调用性能较好。</li>
</ul>
<p>所以，在移动浏览器上，JS的解析和执行时间并不是不可忽略的。</p>
<p>在低端安卓机上，（框架的初始化+异步数据请求+业务代码执行）会远高于几KB网络请求时间；高性能的Web网站需要仔细斟酌前端渲染带来的性能问题。</p>
<h2 id="优化-2"><a href="#优化-2" class="headerlink" title="优化"></a>优化</h2><ul>
<li>高性能要求页面还是需要后端渲染。</li>
<li>React还是太重了，面向用户写系统需要谨慎考虑。</li>
<li>JS代码的编译和执行会有缓存，同App中网页尽量统一框架。</li>
</ul>
<h2 id="WebView性能优化总结"><a href="#WebView性能优化总结" class="headerlink" title="WebView性能优化总结"></a>WebView性能优化总结</h2><p>一个加载网页的过程中，native、网络、后端处理、CPU都会参与，各自都有必要的工作和依赖关系；让他们相互并行处理而不是相互阻塞才可以让网页加载更快：</p>
<ul>
<li>WebView初始化慢，可以在初始化同时先请求数据，让后端和网络不要闲着。</li>
<li>后端处理慢，可以让服务器分trunk输出，在后端计算的同时前端也加载网络静态资源。</li>
<li>脚本执行慢，就让脚本在最后运行，不阻塞页面解析。</li>
<li>同时，合理的预加载、预缓存可以让加载速度的瓶颈更小。</li>
<li>WebView初始化慢，就随时初始化好一个WebView待用。</li>
<li>DNS和链接慢，想办法复用客户端使用的域名和链接。</li>
<li>脚本执行慢，可以把框架代码拆分出来，在请求页面之前就执行好。</li>
</ul>
<h2 id="WebView内存消耗"><a href="#WebView内存消耗" class="headerlink" title="WebView内存消耗"></a>WebView内存消耗</h2><p>分析<br>为了测试WebView会消耗多少内存，我们设计了如下的测试方案：</p>
<ol>
<li>客户端启动后，记录消耗的内存。</li>
<li>打开空页面，记录内存的上涨。</li>
<li>退出。</li>
<li>打开空页面，记录内存上涨。</li>
<li>退出。</li>
<li>打开加载了代码的页面，记录内存的额外增加。</li>
</ol>
<p>得到如下测试结果：</p>
<blockquote>
<p>测试系统： iOS模拟器，Titans 10.0.7</p>
<p>测试系统： OPPO R829T Android 4.2.2</p>
<p>测试方式：测试10次取平均值</p>
</blockquote>
<pre><code>首次打开增加内存    二次打开增加内存    加载KNB+VUE+灵犀
iOS UIWebView    31.1M    5.52M    2M
iOS WKWebView    1.95M    1.6M    2M
Android    32.2M    6.62M    1.7M
</code></pre><p>WKWebView的内存消耗相比其他低了一个数量级，在此方面相当占优。</p>
<p>UIWebView和Android的WebView在首次初始化时都要消耗大量内存，之后每次新建WebView会额外增加一些。</p>
<p>UIWebView的内存占用不会在关闭WebView时主动回收，每次新开WebView都会消耗额外内存。</p>
<p>相比于性能，对于内存的优化可以做的还是比较有限的。</p>
<ul>
<li>WKWebView的内存占用优势比较大（代价是初始化比较慢）。</li>
<li>页面内代码消耗的内存相比与WebView系统的内存消耗相比可以说是很低。</li>
</ul>
<h2 id="WebView体验"><a href="#WebView体验" class="headerlink" title="WebView体验"></a>WebView体验</h2><p>除了打开的速度，WebView通常体验也没有native的实现更好，我们可以找到以下几个例子：</p>
<h2 id="长按选择"><a href="#长按选择" class="headerlink" title="长按选择"></a>长按选择</h2><p>在WebView中，长按文字会使得WebView默认开始选择文字；长按链接会弹出提示是否在新页面打开。</p>
<blockquote>
<p>解决方法：可以通过给body增加CSS来禁止这些默认规则。</p>
</blockquote>
<h2 id="点击延迟"><a href="#点击延迟" class="headerlink" title="点击延迟"></a>点击延迟</h2><p>在WebView中，click通常会有大约300ms的延迟（同时包括链接的点击，表单的提交，控件的交互等任何用户点击行为）。</p>
<p>唯一的例外是设置的meta：viewpoint为禁止缩放的Chrome（然而并不是Android默认的浏览器）。</p>
<blockquote>
<p>解决方法：使用fastclick一般可以解决这个问题。</p>
</blockquote>
<h2 id="页面滑动期间不渲染-执行"><a href="#页面滑动期间不渲染-执行" class="headerlink" title="页面滑动期间不渲染/执行"></a>页面滑动期间不渲染/执行</h2><p>在很多需求中会有一些吸顶的元素，例如导航条，购买按钮等；当页面滚动超出元素高度后，元素吸附在屏幕顶部。</p>
<p>这个功能在PC和native中都能够实现，然而在WebView中却成了难题：</p>
<p>在页面滚动期间，Scroll Event不触发<br>不仅如此，WebView在滚动期间还有各种限定：</p>
<ul>
<li>setTimeout和setInterval不触发。</li>
<li>GIF动画不播放。</li>
<li>很多回调会延迟到页面停止滚动之后。</li>
<li>background-position: fixed不支持。</li>
<li>这些限制让WebView在滚动期间很难有较好的体验。</li>
<li>这些限制大部分是不可突破的，但至少对于吸顶功能还是可以做一些支持：</li>
</ul>
<p>解决方法：</p>
<ul>
<li>在iOS上，使用position: sticky可以做到元素吸顶。</li>
<li>在Android上，监听touchmove事件可以在滑动期间做元素的position切换（惯性运动期间就无效了）。</li>
</ul>
<h2 id="crash"><a href="#crash" class="headerlink" title="crash"></a>crash</h2><p>通常WebView并不能直接接触到底层的API，因此比较稳定；但仍然有使用不当造成整个App崩溃的情况。</p>
<p>目前发现的案例包括：</p>
<ul>
<li>使用过大的图片（2M）</li>
<li>不正常使用WebGL</li>
</ul>
<h2 id="WebView安全"><a href="#WebView安全" class="headerlink" title="WebView安全"></a>WebView安全</h2><p>WebView被运营商劫持、注入问题<br>由于WebView加载的页面代码是从服务器动态获取的，这些代码将会很容易被中间环节所窃取或者修改，其中最主要的问题出自地方运营商（浙江尤其明显）和一些WiFi。</p>
<p>我们监测到的问题包括：</p>
<ul>
<li>无视通信规则强制缓存页面。</li>
<li>header被篡改。</li>
<li>页面被注入广告。</li>
<li>页面被重定向。</li>
<li>页面被重定向并重新iframe到新页面，框架嵌入广告。</li>
<li>HTTPS请求被拦截。</li>
<li>DNS劫持。</li>
</ul>
<p>这些问题轻则影响用户体验，重则泄露数据，或影响公司信誉。</p>
<p>针对页面注入的行为，有一些解决方案：</p>
<h2 id="使用CSP（Content-Security-Policy）"><a href="#使用CSP（Content-Security-Policy）" class="headerlink" title="使用CSP（Content Security Policy）"></a>使用CSP（Content Security Policy）</h2><p>CSP可以有效的拦截页面中的非白名单资源，而且兼容性较好。在美团移动版的使用中，能够阻止大部分的页面内容注入。</p>
<p>但在使用中还是存在以下问题：</p>
<ul>
<li>由于业务的需要，通常inline脚本还是在白名单中，会导致完全依赖内联的页面代码注入可以通过检测。</li>
<li>如果注入的内容是纯HTML+CSS的内容，则CSP无能为力。</li>
<li>无法解决页面被劫持的问题。</li>
<li>会带来额外的一些维护成本。<br>总体来说CSP是一个行之有效的防注入方案，但是如果对于安全要求更高的网站，这些还不够。</li>
</ul>
<h2 id="HTTPS"><a href="#HTTPS" class="headerlink" title="HTTPS"></a>HTTPS</h2><p>HTTPS可以防止页面被劫持或者注入，然而其副作用也是明显的，网络传输的性能和成功率都会下降，而且HTTPS的页面会要求页面内所有引用的资源也是HTTPS的，对于大型网站其迁移成本并不算低。</p>
<p>HTTPS的一个问题在于：一旦底层想要篡改或者劫持，会导致整个链接失效，页面无法展示。这会带来一个问题：本来页面只是会被注入广告，而且广告会被CSP拦截，而采用了HTTPS后，整个网页由于受到劫持完全无法展示。</p>
<p>对于安全要求不高的静态页面，就需要权衡HTTPS带来的利与弊了。</p>
<h2 id="App使用Socket代理请求"><a href="#App使用Socket代理请求" class="headerlink" title="App使用Socket代理请求"></a>App使用Socket代理请求</h2><p>如果HTTP请求容易被拦截，那么让App将其转换为一个Socket请求，并代理WebView的访问也是一个办法。</p>
<p>通常不法运营商或者WiFi都只能拦截HTTP（S）请求，对于自定义的包内容则无法拦截，因此可以基本解决注入和劫持的问题。</p>
<p>Socket代理请求也存在问题。</p>
<ul>
<li>首先，使用客户端代理的页面HTML请求将丧失边下载边解析的能力；根据前面所述，浏览器在HTML收到部分内容后就立刻开始解析，并加载解析出来的外链、图片等，执行内联的脚本……而目前WebView对外并没有暴露这种流式的HTML接口，只能由客户端完全下载好HTML后，注入到WebView中。因此其性能将会受到影响。</li>
<li>其次，其技术问题也是较多的，例如对跳转的处理，对缓存的处理，对CDN的处理等等……稍不留神就会埋下若干大坑。<br>此外还有一些其他的办法，例如页面的MD5检测，页面静态页打包下载等等方式，具体如何选择还要根据具体的场景抉择。</li>
</ul>
<h2 id="客户端内打开第三方WebView"><a href="#客户端内打开第三方WebView" class="headerlink" title="客户端内打开第三方WebView"></a>客户端内打开第三方WebView</h2><p>一般来说，客户端内的WebView都是可以通过客户端的某个schema打开的，而要打开页面的URL很多都并不写在客户端内，而是可以由URL中的参数传递过去的。</p>
<p>那么，一旦此URL可以通过外界输入自定义，那么就有可能在客户端内部打开一个外部的网页。</p>
<p>例：作案过程</p>
<ul>
<li>某个App有个WebView，打开的schema为 appxx://web?url={weburl}。</li>
<li>App中有个扫码的功能，可以扫描某个二维码并打开对应的schema链接。</li>
<li>某个坏人制作了一个二维码并张贴到街上，内容符合 ： appxx://web?url={some_hack_weburl}。</li>
<li>用户扫码打开了some_hack_weburl。</li>
<li>如果some_hack_weburl是一个高仿的登录页面，那么用户将会很可能将用户名密码提交到其他网站。</li>
</ul>
<p>解决方法：在内嵌的WebView中应该限制允许打开的WebView的域名，并设置运行访问的白名单。或者当用户打开外部链接前给用户强烈而明显的提示。</p>
<h2 id="发展"><a href="#发展" class="headerlink" title="发展"></a>发展</h2><p>在一个客户端内，native目前主要功能是提供高效而基础的功能；内部的WebView则添加一些性能体验要求不高但动态化要求高的能力。</p>
<p>提高客户端的动态能力，或者提高WebView的性能，都是提升App功能覆盖的方式。</p>
<p>而目前的各种框架，ReactNative、Week包括微信小程序，都是这个趋势的尝试。</p>
<p>随着技术的发展，WebView的性能、体验和安全问题也将会逐渐的改善，在App中占有越来越多比重的同时，也将会为App开拓新的能力，为用户带来更优质的体验。</p>
<p><img src="https://tech.meituan.com/img/webviewspeed/dest.png" alt="avatar"> </p>

      
    </div>
    
  </div>
  
    
<nav id="article-nav">
  
    <a href="/2018/09/05/iOS RAC的使用总结/" id="article-nav-newer" class="article-nav-link-wrap">
      <strong class="article-nav-caption"><</strong>
      <div class="article-nav-title">
        
          iOS RAC的使用总结
        
      </div>
    </a>
  
  
    <a href="/2018/09/03/深入理解JSCore/" id="article-nav-older" class="article-nav-link-wrap">
      <div class="article-nav-title">深入理解JSCore</div>
      <strong class="article-nav-caption">></strong>
    </a>
  
</nav>

  
</article>


<div class="share_jia">
	<!-- JiaThis Button BEGIN -->
	<div class="jiathis_style">
		<span class="jiathis_txt">Share to: &nbsp; </span>
		<a class="jiathis_button_facebook"></a> 
    <a class="jiathis_button_twitter"></a>
    <a class="jiathis_button_plus"></a> 
    <a class="jiathis_button_tsina"></a>
		<a class="jiathis_button_cqq"></a>
		<a class="jiathis_button_douban"></a>
		<a class="jiathis_button_weixin"></a>
		<a class="jiathis_button_tumblr"></a>
    <a href="http://www.jiathis.com/share" class="jiathis jiathis_txt jtico jtico_jiathis" target="_blank"></a>
	</div>
	<script type="text/javascript" src="http://v3.jiathis.com/code/jia.js?uid=1405949716054953" charset="utf-8"></script>
	<!-- JiaThis Button END -->
</div>






<div class="duoshuo">
	<!-- 多说评论框 start -->
	<div class="ds-thread" data-thread-key="WebView性能、体验分析与优化" data-title="WebView性能、体验分析与优化" data-url="http://yoursite.com/2018/09/04/WebView性能、体验分析与优化/"></div>
	<!-- 多说评论框 end -->
	<!-- 多说公共JS代码 start (一个网页只需插入一次) -->
	<script type="text/javascript">
	var duoshuoQuery = {short_name:"true"};
	(function() {
		var ds = document.createElement('script');
		ds.type = 'text/javascript';ds.async = true;
		ds.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') + '//static.duoshuo.com/embed.js';
		ds.charset = 'UTF-8';
		(document.getElementsByTagName('head')[0] 
		 || document.getElementsByTagName('body')[0]).appendChild(ds);
	})();
	</script>
	<!-- 多说公共JS代码 end -->
</div>




</div>
      <footer id="footer">
  <div class="outer">
    <div id="footer-info">
    	<div class="footer-left">
    		&copy; 2019 Grx
    	</div>
      	<div class="footer-right">
      		<a href="http://hexo.io/" target="_blank">Hexo</a>  Theme <a href="https://github.com/litten/hexo-theme-yilia" target="_blank">Yilia</a> by Litten
      	</div>
    </div>
  </div>
</footer>
    </div>
    
  <link rel="stylesheet" href="/fancybox/jquery.fancybox.css">


<script>
	var yiliaConfig = {
		fancybox: true,
		mathjax: true,
		animate: true,
		isHome: false,
		isPost: true,
		isArchive: false,
		isTag: false,
		isCategory: false,
		open_in_new: false
	}
</script>
<script src="http://7.url.cn/edu/jslib/comb/require-2.1.6,jquery-1.9.1.min.js"></script>
<script src="/js/main.js"></script>






<script type="text/x-mathjax-config">
MathJax.Hub.Config({
    tex2jax: {
        inlineMath: [ ['$','$'], ["\\(","\\)"]  ],
        processEscapes: true,
        skipTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
    }
});

MathJax.Hub.Queue(function() {
    var all = MathJax.Hub.getAllJax(), i;
    for(i=0; i < all.length; i += 1) {
        all[i].SourceElement().parentNode.className += ' has-jax';                 
    }       
});
</script>

<script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
</script>


  </div>
</body>
</html>